# -*- coding: utf-8 -*-
#
# Copyright 2009 Vanderbilt University
# 
# Licensed under the Apache License, Version 2.0 (the "License"); 
# you may not use this file except in compliance with the License. 
# You may obtain a copy of the License at 
# 
#     http://www.apache.org/licenses/LICENSE-2.0 
# 
# Unless required by applicable law or agreed to in writing, software 
# distributed under the License is distributed on an "AS IS" BASIS, 
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
# See the License for the specific language governing permissions and 
# limitations under the License. 
"""

.. moduleauthor:: John Paulett <john.paulett -at- vanderbilt.edu>
"""
from hornet.event import is_accepted_event, EventNotAccepted
from hornet.file import join
from functools import wraps
import logging

logger = logging.getLogger(__name__)

def valid_plugin(plugin):
    """Ensures that *plugin* is a subclass of :class:`hornet.plugin.Plugin` and 
    implements the proper methods. If *plugin* is not valid, a
    :class:`hornet.plugin.InvalidPlugin` exception is raised.
    """
    if not isinstance(plugin, Plugin):
        raise InvalidPlugin, 'Not a subclass of Plugin'

def log(func, *args, **kw):
    """Decorator that will print debug statements at the start and end of
    execution of *func*. Expects *func* to be a method of an instance of
    :class:`hornet.plugin.Plugin`. If using :func:`hornet.plugin.log` 
    and :func:`hornet.plugin.notify_on_finish` on the same method, it
    is recommended to wrap :func:`hornet.plugin.log` inside of
    :func:`hornet.plugin.notify_on_finish` to ensure that the finished log
    statement is printed before all the listeners are notified.
    """
    @wraps(func)
    def new(*args, **kw):
        self = args[0]
        info = (self.id, func.__name__)
        logger.debug('Plugin, "%s", started "%s"' % info)
        retval = func(*args, **kw)
        logger.debug('Plugin, "%s", finished "%s"' % info)
        return retval
    return new

def notify_on_finish(func, *args, **kw):
    """Decorator that will notify all the listeners of the the plugin once
    *func* is complete. The notification event, a subclass of 
    :class:`hornet.event.Event` must be returned by *func*. 
    Expects *func* to be a method of an instance of
    :class:`hornet.plugin.Plugin`.
    """
    @wraps(func)
    def new(*args, **kw):
        self = args[0]
        event = func(*args, **kw)
        self.notify_listeners(event)
        return event
    return new

def wrap_notify(func, plugin):
    """Decorator that ensures the event passed to the notify function
    is on of the allowed Events, according to accepted_events.
    """
    @wraps(func)
    def new(event):
        if is_accepted_event(event, plugin.accepted_events()):
            return func(event)
        else:
            raise EventNotAccepted, \
                '%s not accepted by plugin, %s' % (event, plugin.id)
    return new

def wrap_setup(func, plugin):
    """Decorator that ensures that the :meth:`hornet.plugin.Plugin.setup`
    method performs certain actions even if overridden. The setup 
    actions include registering plugins.
    """
    @wraps(func)
    def new():
        for announcers in plugin.listen_to:
            announcers.register(plugin)
        logger.debug('Plugin, "%s", setup' % plugin.id)
        func()
    return new

def wrap_teardown(func, plugin):
    """Decorator that ensures that the :meth:`hornet.plugin.Plugin.teardown`
    method performs certain actions even if overridden. The teardown 
    actions include unregistering plugins and ensures that any errors
    do not get raised.
    """
    @wraps(func)
    def new():
        for announcer in plugin.listen_to:
            try:
                announcer.unregister(plugin)
            except Exception:
                logger.error('Error unregistering the plugin %s' % plugin.id)
        try:
            func()
        except Exception:
            logger.error('Error tearing down the plugin %s' % plugin.id)
        logger.debug('Plugin, "%s", destroyed' % plugin.id)
    return new

class InvalidPlugin(Exception): 
    """The plugin is not a proper implementation of 
    :class:`hornet.plugin.Plugin`
    """
    
class Plugin(object):
    """Abstract class for plugins. Plugins must subclass this class in order
    to be properly loaded and executed by HORNET.
    """
        
    def setup(self):
        """Method that is called prior to the start of the plugin.
        Subclasses should override this method if they which to have certain
        actions performed upon initialization of the object.
        """
        # subclass if needed
    
    def teardown(self):
        """Method that is called when HORNET is stopping a plugin. 
        Plugins that hold onto resources or large data items should implement
        the code necessary to close these resources and delete anything.
        Any exceptions that occur within this method will be caught and logged
        but will not be rethrown.  However, care should be given to ensure
        that an exception does not prevent other resources from being freed.
        """
        # subclass if needed
    
    def notify(self, event):
        """Method of running the plugin, Called by the plugins 
        which are set in :attr:`hornet.plugin.Plugin.listen_to`.
        """
        # must be subclassed
    
    def accepted_events(self):
        """Returns the set of :class:`hornet.event.Event` subclasses that the
        :meth:`hornet.plugin.Plugin.notify` method with accept. The passing of 
        Events of any other type to the notify method could potentially 
        raise an exception.  Can return either a single class or a tuple
        of classes.
        """
        # must be subclassed
        return None
    
    ########################
    def __new__(cls, *args, **kw):
        """Force several variables to be created without forcing subclasses
        to call super().__init__(). Also adds decorators to subclass-able
        methods.
        """
        obj = object.__new__(cls, *args, **kw)
        ## identifier of the instance of the plugin
        obj._id = ''
        
        ## set of plugins that must be notified of changes within in this
        ## instance of the plugin plugin
        obj._listeners = []
        
        ## dynamically register to listen to these plugins
        obj.listen_to = []
        
        ## decorate the implementation of notify to ensure proper arguments
        obj.notify = wrap_notify(obj.notify, obj)
        obj.setup = wrap_setup(obj.setup, obj)
        obj.teardown = wrap_teardown(obj.teardown, obj)
        
        return obj
    
    def register(self, listener):
        """Add *listener* to the list of listeners of this plugin."""
        self._listeners.append(listener)
 
    def unregister(self, listener):
        """Removes *listener* from the list of listeners of this plugin."""
        self._listeners.remove(listener)
    
    def listeners(self):
        """Returns the list of all the listeners of this plugin."""
        return self._listeners
 
    def notify_listeners(self, event):
        """Method for notifying all the listeners of an event.  Should be 
        called at any point which the plugin should inform other plugins of its
        changes.
        """
        event._caller = self.id
        for listener in self._listeners:
            listener.notify(event)
    
    def output(self, filename):
        """Returns the location to which to save *filename* based upon 
        the *output_dir* defined in :class:`hornet.application.Config`.
        """
        return join(self.parent.config.output_dir, filename)

    @property
    def type(self):
        """Type of the plugin instance, which is simply the module and 
        class name of the type of the plugin in a tuple.
        """
        cls = self.__class__
        return (cls.__module__, cls.__name__)

    @property
    def id(self):
        """A unique identifier for the plugin instance. Typically set by 
        HORNET based upon the :ref:`configuration <hornet_configuration>` of 
        the plugin.
        """
        return self._id
    
    @property
    def parent(self):
        """Reference to the main :class:`hornet.application.Hornet`.  Useful
        in select contexts.
        """
        return self._parent
